package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/client"
	"github.com/gorilla/websocket"
)

var (
	ID         = "7c499e1395c932cdb7cf163f2831056fb3327154e05723214fb391330aab9ab5"
	Dir        = "../dcodebox"
	FileIgnore = map[string]bool{".git": true}
)
var upgrader = &websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
	Subprotocols: []string{"Sec-WebSocket-Protocol"},
}

func main() {
	http.HandleFunc("/ws", ws)
	http.HandleFunc("/content/", content)
	http.ListenAndServe(":8899", nil)
}

func content(w http.ResponseWriter, r *http.Request) {
	filename := r.URL.Path[len("/content/"):]
	path := fmt.Sprintf("%s/%s", Dir, filename)
	switch r.Method {
	case "GET":
		file, err := os.Stat(path)
		if err != nil {
			http.Error(w, err.Error(), 505)
			return
		}
		if file.IsDir() {
			files, err := ReadDir(filename)
			if err != nil {
				http.Error(w, err.Error(), 505)
				return
			}
			b, err := json.Marshal(files)
			if err != nil {
				http.Error(w, err.Error(), 505)
				return
			}
			w.Write(b)
		} else {
			http.ServeFile(w, r, path)
		}
	case "POST":
		err := os.MkdirAll(filepath.Dir(path), 0755)
		if err != nil {
			http.Error(w, err.Error(), 505)
			return
		}
		b, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), 505)
			return
		}
		err = os.WriteFile(path, b, 0644)
		if err != nil {
			http.Error(w, err.Error(), 505)
			return
		}
	case "DELETE":
		err := os.Remove(path)
		if err != nil {
			http.Error(w, err.Error(), 505)
			return
		}
	}
	w.WriteHeader(200)
}

type FileInfo struct {
	Name     string     `json:"name"`
	Size     int64      `json:"size"`
	ModTime  time.Time  `json:"mod_time"`
	IsDir    bool       `json:"is_dir"`
	Path     string     `json:"path"`
	Children []FileInfo `json:"children"`
}

func ReadDir(filename string) ([]FileInfo, error) {
	dir := fmt.Sprintf("%s%s", Dir, filename)
	info, err := os.Stat(dir)
	if err != nil {
		return []FileInfo{}, err
	}
	if !info.IsDir() {
		return []FileInfo{}, fmt.Errorf("%s is not a directory", dir)
	}
	var items []FileInfo
	files, _ := os.ReadDir(dir)
	for _, f := range files {
		info, err := f.Info()
		if err != nil {
			return []FileInfo{}, err
		}
		if _, ok := FileIgnore[info.Name()]; ok {
			continue
		}
		item := FileInfo{
			Name:    info.Name(),
			Size:    info.Size(),
			ModTime: info.ModTime(),
			IsDir:   info.IsDir(),
			Path:    fmt.Sprintf("%s/%s", filename, f.Name()),
		}
		if item.IsDir {
			children, err := ReadDir(item.Path)
			if err != nil {
				return []FileInfo{}, err
			}
			item.Children = children
		}
		items = append(items, item)
	}
	return items, nil
}

func ws(w http.ResponseWriter, r *http.Request) {
	wsConn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		fmt.Println(err)
		return
	}
	execConn, err := exec()
	if err != nil {
		fmt.Println(err)
		return
	}
	errorChan := make(chan error, 1)
	go execToWS(execConn, wsConn, errorChan)
	go wsToExec(wsConn, execConn, errorChan)
	err = <-errorChan
	if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
		log.Fatal(err)
	}
}

func execToWS(execConn net.Conn, wsConn *websocket.Conn, errorChan chan error) {
	for {
		b := make([]byte, 2048)
		_, err := execConn.Read(b)
		if err != nil {
			errorChan <- err
			break
		}
		err = wsConn.WriteMessage(websocket.TextMessage, []byte(b))
		if err != nil {
			errorChan <- err
			break
		}
	}
}
func wsToExec(wsConn *websocket.Conn, execConn net.Conn, errorChan chan error) {
	for {
		_, b, err := wsConn.ReadMessage()
		if err != nil {
			errorChan <- err
			break
		}
		_, err = execConn.Write(b)
		if err != nil {
			errorChan <- err
			break
		}
	}
}
func exec() (net.Conn, error) {
	ctx := context.Background()
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		return nil, err
	}
	defer cli.Close()
	idRes, err := cli.ContainerExecCreate(ctx, ID, types.ExecConfig{
		AttachStdin:  true,
		AttachStdout: true,
		AttachStderr: true,
		Cmd:          []string{"bash"},
		Tty:          true,
		WorkingDir:   "/root",
	})
	if err != nil {
		return nil, err
	}
	hijkRes, err := cli.ContainerExecAttach(ctx, idRes.ID, types.ExecStartCheck{Detach: false, Tty: true})
	if err != nil {
		return nil, err
	}
	return hijkRes.Conn, nil
}
